﻿using System;
using System.ComponentModel;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
using System.Windows;
using System.Reflection;
using System.Windows.Data;
using System.Diagnostics.Contracts;
using System.Data;
using E = System.Linq.Expressions.Expression;
using OE = ReactiveCode.Observables.Expressions.ObservableExpressionExtensions;

namespace ReactiveCode.Observables
{
    using Util;
    using Util.Expressions;
    using Observables;
    using Expressions;

    using Convert = Func<E, E>;
    using ConvertWithArgs = Func<E, E[], E>;

    partial class ChangeListening
    {
        static ChangeListening()
        {
#if CONTRACT_DEBUGGING
            ContractDebugging.Initialize();
#endif

            RegisterPropertyExpressionConverter(NotifyPropertyChangedConverter);
            RegisterPropertyExpressionConverter(DependencyPropertyConverter);
            RegisterMethodExpressionConverter(AttachedDependencyPropertyConverter);
            RegisterPropertyExpressionConverter(NamedEventConverter);
            RegisterConverter(TimeConverter);
            RegisterPropertyExpressionConverter(ObservableValueConverter);
        }

        #region Named events

        static Convert NamedEventConverter(PropertyInfo property)
        {
            Contract.Requires(property != null);
            var bindingFlags = BindingFlags.Public | (property.IsStatic() ? BindingFlags.Static : BindingFlags.Instance);
            var changedEvent = property.DeclaringType.GetEvent(property.Name + "Changed", bindingFlags);
            if (changedEvent == null)
                return null;

            return instance =>
            {
                var events = OE.FromEvent(instance, changedEvent).AsUnit().WithInitial();
                return events.Select(E.Lambda(
                    E.Property(instance, property),
                    E.Parameter(typeof(Unit))
                ));
            };
        }

        #endregion

        #region INotifyPropertyChanged

        static MethodInfo _observePropertyChanged = typeof(Observables.Extensions).GetMethod("ObservePropertyChanged", BindingFlags.Static | BindingFlags.NonPublic | BindingFlags.Public);
        static Convert NotifyPropertyChangedConverter(PropertyInfo property)
        {
            if (property.IsStatic() || !typeof(INotifyPropertyChanged).IsAssignableFrom(property.DeclaringType))
                return null;

            return instance =>
            {
                Contract.Assume(_observePropertyChanged != null);
                var events = E.Call(_observePropertyChanged, instance).AsUnit().WithInitial();
                return events.Select(E.Lambda(
                    E.Property(instance, property),
                    E.Parameter(typeof(Unit))
                ));
            };
        }

        #endregion

        #region Dependency properties

        static DependencyProperty FindDependencyProperty(Type containerType, string dependencyPropertyName)
        {
            var depPropField = containerType.GetField(dependencyPropertyName + "Property", BindingFlags.Public | BindingFlags.Static);
            if (depPropField == null || !typeof(DependencyProperty).IsAssignableFrom(depPropField.FieldType))
                return null;
            return (DependencyProperty)depPropField.GetValue(null);
        }

        static MethodInfo _observeDependencyProperty = typeof(Observables.Extensions).GetMethod("ObserveDependencyProperty", BindingFlags.Static | BindingFlags.NonPublic | BindingFlags.Public);
        static E ObserveDependencyProperty(E instance, DependencyProperty property)
        {
            Contract.Assume(_observeDependencyProperty != null);
            return E.Call(_observeDependencyProperty, instance, E.Constant(property)).Cast(property.PropertyType);
        }
        static Convert DependencyPropertyConverter(PropertyInfo property)
        {
            Contract.Requires(property != null);
            if (property.IsStatic() || !typeof(DependencyObject).IsAssignableFrom(property.DeclaringType))
                return null;

            var depProp = FindDependencyProperty(property.DeclaringType, property.Name);
            if (depProp == null)
                return null;

            return instance => ObserveDependencyProperty(instance, depProp);
        }
        static ConvertWithArgs AttachedDependencyPropertyConverter(MethodInfo method)
        {
            if (method == null || method.DeclaringType == null || !method.IsStatic || !method.Name.StartsWith("Get") || method.IsGenericMethod)
                return null;
            var parameters = method.GetParameters();
            if (parameters.Length != 1 || !typeof(DependencyObject).IsAssignableFrom(parameters[0].ParameterType))
                return null;

            var depProp = FindDependencyProperty(method.DeclaringType, method.Name.Substring(3));
            if (depProp == null)
                return null;

            return (obj, args) =>
            {
                Contract.Assume(obj == null && args.Length == 1);
                return ObserveDependencyProperty(args[0], depProp);
            };
        }

        #endregion

        #region Time

        static TimeSpan _defaultTimerInterval = TimeSpan.FromSeconds(1);
        public static TimeSpan DefaultTimerInterval
        {
            get { return _defaultTimerInterval; }
            set
            {
                Contract.Requires<ArgumentException>(value > TimeSpan.Zero, "value");
                _defaultTimerInterval = value;
            }
        }
        static E TimeConverter(E expression, Func<E, E> convertSubexpression)
        {
            E observableInterval = null;
            var call = expression as MethodCallExpression;
            if (call != null && call.Method.DeclaringType == typeof(ChangeListening) && call.Method.Name == "Precision" && call.Type == typeof(DateTime))
            {
                Contract.Assume(call.Arguments.Count == 2);
                Contract.Assume(call.Arguments[0].Type == typeof(DateTime));
                Contract.Assume(call.Arguments[1].Type == typeof(TimeSpan));
                expression = call.Arguments[0];
                observableInterval = convertSubexpression(call.Arguments[1]);
            }

            var memberExpression = expression as MemberExpression;
            if (memberExpression == null)
                return null;
            var property = memberExpression.Member as PropertyInfo;
            if (property == null)
                return null;

            bool isAboutTime = (property.DeclaringType == typeof(DateTime) && (property.Name == "Today" || property.Name == "Now")) ||
                               (property.DeclaringType == typeof(Environment) && property.Name == "TickCount");
            if (!isAboutTime)
                return null;

            E interval;
            if (observableInterval != null)
                interval = E.Parameter(typeof(TimeSpan));
            else
            {
                if (property.Name == "TickCount")
                {
                    interval = E.Call(typeof(TimeSpan), "FromTicks", Type.EmptyTypes, E.Constant(1L));
                }
                else
                {
                    var defaultTimerInterval = typeof(ChangeListening).GetProperty("DefaultTimerInterval", BindingFlags.Public | BindingFlags.Static);
                    interval = E.Property(null, defaultTimerInterval);
                }
            }

            var zero = typeof(TimeSpan).GetField("Zero", BindingFlags.Public | BindingFlags.Static);
            var timer = OE.Timer(E.Field(null, zero), interval);

            if (observableInterval != null)
                timer = observableInterval.SelectMany(E.Lambda(timer, (ParameterExpression)interval));

            return timer
                .Select(E.Lambda(expression, E.Parameter(typeof(long), "_")))
                .DistinctUntilChanged();
        }

        public static DateTime Precision(this DateTime time, TimeSpan value)
        {
            return time;
        }

        #endregion

        #region IObservableValue

        static Convert ObservableValueConverter(PropertyInfo property)
        {
            if (property.DeclaringType == null || property.Name != "Value" || !typeof(IObservableValue<>).MakeGenericType(property.PropertyType).IsAssignableFrom(property.DeclaringType))
                return null;

            return observable => observable.ExpectType(typeof(IObservable<>).MakeGenericType(property.PropertyType));
        }

        #endregion
    }
}
